﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Web;

namespace ifunction.BaiduBCS.SDK
{
    /// <summary>
    /// Class BaiduBCSClient.
    /// This class is to provide REST-API based interfaces for Baidu BCS, following official documentation: http://developer.baidu.com/wiki/index.php?title=docs/cplat/stor/api
    /// 
    /// <example>
    /// The following code would show you how to create, get and delete <c>bucket</c> in <c>BaiduBCSClient</c>
    /// <code>
    /// <![CDATA[
    /// // You need to assign your bucket in your code
    /// string bucket = "TestBucket";
    /// // You need to assign your app key and your secret key in your code
    /// var client = new BaiduBCSClient("your app key", "your secret key");
    ///
    /// // According to bucket naming rule by Baidu, all bucket names would be changed to lower case inside client for any bucket operation.
    /// client.CreateBucket(bucket);
    ///
    /// // Get bucket list and output summary via Console.
    /// var bucketList = client.GetBucketList();
    ///
    /// foreach (var one in bucketList)
    /// {
    ///     Console.WriteLine(string.Format("{Bucket: {0}, TotalCapacity: {1}.", one.Name, one.TotalCapacity));
    /// }
    ///
    /// // Delete bucket.
    /// client.DeleteBucket(bucket);
    /// ]]>
    /// </code>
    /// 
    /// The following code would show you how to create, get and delete <c>object</c> in <c>BaiduBCSClient</c>
    /// <code>
    /// <![CDATA[
    /// // You need to assign your bucket in your code
    /// string bucket = "TestBucket";
    /// // You need to assign your object name (file name) in your code
    /// string objectName = "test.txt";
    /// // You need to assign your app key and your secret key in your code
    /// var client = new BaiduBCSClient("your app key", "your secret key");
    ///
    /// // Firstly, we need to create a file for upload test later.
    /// //Supposed that the test machine has disk D and application has enough permission to operate on that.
    /// File.WriteAllText(@"D:\" + objectName, "Test string \n\r Hello world.");
    ///
    /// // Read/Load the bytes for creating object.
    /// byte[] content = File.ReadAllBytes(@"D:\" + objectName);
    ///
    /// // Upload bytes and file information
    /// var md5 = client.CreateStorageObject(new StorageObjectUploadRequest
    ///       {
    ///           Bytes = content,
    ///           BucketName = bucket,
    ///           ObjectName = objectName,
    ///           DownloadFileName = "DownloadName.dat",
    ///           ContentType = "text/plain",
    ///           IsSuperFile = false,
    ///           Permission = StorageObjectPermission.PublicReadAndWrite
    ///       });
    ///
    /// // We try to get meta data for the uploaded file.
    /// var meta = client.GetStorageObjectMetaInfo(new StorageObjectDownloadRequest
    /// {
    ///     BucketName = bucket,
    ///     ObjectName = objectName
    /// });
    ///
    /// // We try to get whole byte data for the uploaded file.
    /// var data = client.GetStorageObject(new StorageObjectDownloadRequest
    /// {
    ///     BucketName = bucket,
    ///     ObjectName = objectName
    /// });
    ///
    /// // We also can save file in local by specific path
    /// client.SaveStorageObjectAs(new StorageObjectDownloadRequest
    /// {
    ///     BucketName = bucket,
    ///     ObjectName = objectName
    /// }, @"D:\DownloadedTest.txt");
    ///
    /// // Test file needs to be deleted.
    /// client.DeleteStorageObject(new StorageObjectIdentity
    /// {
    ///     BucketName = bucket,
    ///     ObjectName = objectName
    /// });
    /// ]]>
    /// </code>
    /// </example>
    /// </summary>
    public partial class BaiduBCSClient
    {
        /// <summary>
        /// The base service URL
        /// </summary>
        protected const string baseServiceUrl = "http://bcs.duapp.com/";

        /// <summary>
        /// Gets or sets the application key.
        /// </summary>
        /// <value>The application key.</value>
        public string AppKey { get; protected set; }

        /// <summary>
        /// Gets or sets the secret key.
        /// </summary>
        /// <value>The secret key.</value>
        public string SecretKey { get; protected set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="BaiduBCSClient"/> class.
        /// </summary>
        /// <param name="appKey">The application key.</param>
        /// <param name="secretKey">The secret key.</param>
        public BaiduBCSClient(string appKey, string secretKey)
        {
            this.AppKey = appKey;
            this.SecretKey = secretKey;
        }

        #region Protected util

        /// <summary>
        /// Creates the HTTP request.
        /// </summary>
        /// <param name="featureUrl">The feature URL.</param>
        /// <param name="httpVerb">The HTTP verb.</param>
        /// <param name="bucketName">Name of the bucket.</param>
        /// <param name="objectName">Name of the object.</param>
        /// <param name="effectDueStamp">The effect due stamp.</param>
        /// <param name="ipLimitation">The ip limitation.</param>
        /// <param name="sizeLimitation">The size limitation.</param>
        /// <returns>HttpWebRequest.</returns>
        protected HttpWebRequest CreateHttpRequest(string featureUrl, string httpVerb, string bucketName, string objectName, DateTime? effectDueStamp = null, string ipLimitation = null, long? sizeLimitation = null)
        {
            objectName = "/" + objectName.GetStringValue().TrimStart('/');
            string originalUrl = baseServiceUrl + featureUrl;

            string sign = CreateSign(httpVerb, bucketName, objectName, effectDueStamp, ipLimitation, sizeLimitation);
            string completeUrl = string.Format("{0}{1}sign={2}", originalUrl, originalUrl.IndexOf('?') > -1 ? "&" : "?", sign);

            if (effectDueStamp != null)
            {
                completeUrl += ("&time=" + effectDueStamp.Value.ToJavaScriptSecondsDateTime().ToString());
            }

            if (sizeLimitation != null)
            {
                completeUrl += ("&size=" + sizeLimitation.Value.ToString());
            }

            return this.CreateHttpWebRequest(completeUrl, httpVerb);
        }

        /// <summary>
        /// Throws the exception by status code.
        /// </summary>
        /// <param name="statusCode">The status code.</param>
        /// <param name="message">The message.</param>
        /// <exception cref="System.InvalidOperationException"></exception>
        protected void TryThrowExceptionByStatusCode(HttpStatusCode statusCode, string message)
        {
            if (statusCode != HttpStatusCode.OK)
            {
                throw new InvalidOperationException(string.Format("Http status: {0} ({1}), Message: {2}", statusCode.ToString(), (int)statusCode, message));
            }
        }

        /// <summary>
        /// Creates the feature URL.
        /// </summary>
        /// <param name="bucketName">Name of the bucket.</param>
        /// <param name="objectName">Name of the object.</param>
        /// <returns>System.String.</returns>
        protected static string CreateFeatureUrl(string bucketName, string objectName)
        {
            return string.Format("{0}/{1}", bucketName, objectName);
        }

        /// <summary>
        /// Creates the feature URL.
        /// </summary>
        /// <param name="obj">The object.</param>
        /// <returns>System.String.</returns>
        protected static string CreateFeatureUrl(IBucketObject obj)
        {
            return string.Format("{0}/{1}", obj.BucketName, obj.ObjectName);
        }

        /// <summary>
        /// Checks the bucket object.
        /// </summary>
        /// <param name="obj">The object.</param>
        protected void CheckBucketObject(IBucketObject obj)
        {
            obj.CheckNullObject("IBucketObject");
            obj.BucketName.CheckNullObject("IBucketObject.BucketName");
            obj.ObjectName.CheckNullObject("IBucketObject.ObjectName");

            obj.BucketName = obj.BucketName.ToLowerInvariant();
            obj.ObjectName = obj.ObjectName.ToLowerInvariant();
        }

        /// <summary>
        /// Fills the HTTP request header.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <param name="header">The header.</param>
        /// <param name="headerValue">The header value.</param>
        /// <param name="defaultValue">The default value.</param>
        protected void FillHttpRequestHeader(HttpWebRequest request, string header, string headerValue, string defaultValue = null)
        {
            if (request != null && !string.IsNullOrWhiteSpace(header))
            {
                var value = string.IsNullOrWhiteSpace(headerValue) ? defaultValue : headerValue;

                if (!string.IsNullOrWhiteSpace(value))
                {
                    request.Headers.Set(header, value);
                }
            }
        }

        #endregion

        #region Sign generation

        /// <summary>
        /// Creates the sign.
        /// </summary>
        /// <param name="httpVerb">The HTTP verb.</param>
        /// <param name="bucketName">Name of the bucket.</param>
        /// <param name="objectName">Name of the object.</param>
        /// <param name="effectDueStamp">The effect due stamp.</param>
        /// <param name="ipLimitation">The ip limitation.</param>
        /// <param name="sizeLimitation">The size limitation.</param>
        /// <returns>System.String.</returns>
        protected string CreateSign(string httpVerb, string bucketName, string objectName, DateTime? effectDueStamp = null, string ipLimitation = null, long? sizeLimitation = null)
        {
            var flag = CreateSignFlag(effectDueStamp != null, !string.IsNullOrWhiteSpace(ipLimitation), sizeLimitation != null);
            string content = CreateSignContent(flag, httpVerb, bucketName, objectName, effectDueStamp, ipLimitation, sizeLimitation);
            string hash = content.ToHMACSHA1(this.SecretKey);

            return string.Format("{0}:{1}:{2}", flag, this.AppKey, HttpUtility.UrlEncode(hash));
        }

        /// <summary>
        /// Creates the sign flag.
        /// </summary>
        /// <param name="hasTimeLimitation">The has time limitation.</param>
        /// <param name="hasIpLImitation">The has ip l imitation.</param>
        /// <param name="hasSizeLimitation">The has size limitation.</param>
        /// <returns>System.String.</returns>
        protected string CreateSignFlag(bool hasTimeLimitation, bool hasIpLImitation, bool hasSizeLimitation)
        {
            return string.Format("MBO{0}{1}{2}",
                hasTimeLimitation ? "T" : "",
                hasIpLImitation ? "I" : "",
                hasSizeLimitation ? "S" : "");
        }

        /// <summary>
        /// Creates the content of the sign.
        /// </summary>
        /// <param name="flag">The flag.</param>
        /// <param name="httpVerb">The HTTP verb.</param>
        /// <param name="bucketName">Name of the bucket.</param>
        /// <param name="objectName">Name of the object.</param>
        /// <param name="effectDueStamp">The effect due stamp.</param>
        /// <param name="ipLimitation">The ip limitation.</param>
        /// <param name="sizeLimitation">The size limitation.</param>
        /// <returns>System.String.</returns>
        protected string CreateSignContent(string flag, string httpVerb, string bucketName, string objectName, DateTime? effectDueStamp = null, string ipLimitation = null, long? sizeLimitation = null)
        {
            const string format = "{0}\nMethod={1}\nBucket={2}\nObject={3}\n{4}{5}{6}";

            return string.Format(format,
                flag,
                httpVerb,
                bucketName,
                objectName,
                effectDueStamp != null ? CreateSignKeyValueString("Time", effectDueStamp.Value.ToJavaScriptSecondsDateTime().ToString()) : string.Empty,
                !string.IsNullOrWhiteSpace(ipLimitation) ? CreateSignKeyValueString("Ip", ipLimitation.Trim()) : string.Empty,
                sizeLimitation != null ? CreateSignKeyValueString("Size", sizeLimitation.Value.ToString()) : string.Empty
                );
        }

        /// <summary>
        /// Creates the sign key value string.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="value">The value.</param>
        /// <returns>System.String.</returns>
        protected string CreateSignKeyValueString(string key, string value)
        {
            const string keyValue = "{0}={1}\n";

            return string.Format(keyValue, key, value);
        }

        #endregion
    }
}
